package com.jerome.election.json;

import com.jerome.election.util.ReflectUtil;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * This is the main class for a simple JSON parser. It provides a parse method to parse JSON strings.
 * The parser calls the JsonLexer class to read characters one by one and perform parsing, ultimately returning a parsed JSON object.
 * <p>
 * References:
 * - https://www.tabnine.com/code/java/classes/javax.json.stream.JsonParser
 * - https://www.geeksforgeeks.org/java-program-to-implement-a-json-parser/
 * Simple JSON parser without strict error handling
 *
 * @author Jerome
 * @date 2023/9/9
 */
public class JsonParser {

    /**
     * This method takes a JSON string as input and returns the parsed JSON object.
     *
     * @param json The JSON string to parse.
     * @return The parsed JSON object.
     * @throws JsonParseException If an error occurs during parsing.
     */
    public static Object parse(String json) throws JsonParseException {
        JsonLexer lexer = new JsonLexer(json);
        JsonParser parser = new JsonParser(lexer);
        return parser.parseValue();
    }

    /**
     * This method takes a JSON string as input and returns the parsed JSON object.
     *
     * @param json  The JSON string to parse.
     * @param clazz The class to which the JSON object should be converted.
     * @return The parsed JSON object.
     * @throws JsonParseException     If an error occurs during parsing.
     * @throws InstantiationException If an error occurs during instantiation.
     * @throws IllegalAccessException If an error occurs due to illegal access.
     */
    public static <T> T parse(String json, Class<T> clazz) throws JsonParseException, InstantiationException, IllegalAccessException {
        JsonLexer lexer = new JsonLexer(json);
        JsonParser parser = new JsonParser(lexer);
        Object o = parser.parseValue();
        if (o instanceof Map) {
            // Convert the map into the object we need, currently supporting first-level objects
            return ReflectUtil.build(o, clazz);
        } else {
            //An exception may occur here, so we will not handle it for now.
            //todo
            return (T) o;
        }
    }

    private JsonLexer lexer;

    private JsonParser(JsonLexer lexer) {
        this.lexer = lexer;
    }

    /**
     * This method is the core method of the parser. It performs parsing based on the type of JSON value
     * (object, array, string, number, etc.).
     * Currently only map and Array are supported
     *
     * @return The parsed JSON value.
     * @throws JsonParseException If an error occurs during parsing.
     */
    private Object parseValue() throws JsonParseException {
        lexer.skipWhitespace();

        char nextChar = lexer.peek();
        if (nextChar == '{') {
            return parseMap();
        } else if (nextChar == '[') {
            return parseArray();
        } else if (nextChar == '"') {
            return parseString();
        } else if (Character.isDigit(nextChar) || nextChar == '-') {
            return parseNumber();
        } else if (lexer.matches("true")) {
            lexer.consume("true");
            return true;
        } else if (lexer.matches("false")) {
            lexer.consume("false");
            return false;
        } else if (lexer.matches("null")) {
            lexer.consume("null");
            return null;
        } else {
            throw new JsonParseException("Unexpected character: " + nextChar);
        }
    }

    /**
     * This method is used to parse JSON objects.
     *
     * @return The parsed JSON object.
     * @throws JsonParseException If an error occurs during parsing.
     */
    private Map<String, Object> parseMap() throws JsonParseException {
        lexer.consume('{');
        Map<String, Object> object = new HashMap<>();

        while (!lexer.matches("}")) {
            lexer.skipWhitespace();
            String key = parseString();
            lexer.skipWhitespace();
            lexer.consume(':');
            lexer.skipWhitespace();
            Object value = parseValue();
            object.put(key, value);

            lexer.skipWhitespace();
            if (!lexer.matches(",")) {
                break;
            } else {
                lexer.consume(',');
            }
        }

        lexer.skipWhitespace();
        lexer.consume('}');
        return object;
    }

    /**
     * This method is used to parse JSON arrays.
     *
     * @return The parsed JSON array.
     * @throws JsonParseException If an error occurs during parsing.
     */
    private List<Object> parseArray() throws JsonParseException {
        lexer.consume('[');
        List<Object> array = new ArrayList<>();

        while (!lexer.matches("]")) {
            lexer.skipWhitespace();
            Object value = parseValue();
            array.add(value);

            lexer.skipWhitespace();
            if (!lexer.matches(",")) {
                break;
            } else {
                lexer.consume(',');
            }
        }

        lexer.skipWhitespace();
        lexer.consume(']');
        return array;
    }

    /**
     * This method is used to parse JSON strings and handles escape characters.
     *
     * @return The parsed JSON string.
     * @throws JsonParseException If an error occurs during parsing.
     */
    private String parseString() throws JsonParseException {
        lexer.consume('"');
        StringBuilder sb = new StringBuilder();

        while (true) {
            char c = lexer.next();
            if (c == '"') {
                break;
            } else if (c == '\\') {
                char escape = lexer.next();
                if (escape == 'n') {
                    sb.append('\n');
                } else if (escape == 'r') {
                    sb.append('\r');
                } else if (escape == 't') {
                    sb.append('\t');
                } else {
                    sb.append(escape);
                }
            } else if (c == 0) {
                throw new JsonParseException("Not a normal json");
            }else {
                sb.append(c);
            }
        }

        return sb.toString();
    }

    /**
     * This method is used to parse JSON numbers, handling both integers and floating-point numbers.
     *
     * @return The parsed JSON number.
     * @throws JsonParseException If an error occurs during parsing.
     */
    private Number parseNumber() throws JsonParseException {
        StringBuilder sb = new StringBuilder();
        boolean isFloatingPoint = false;

        while (Character.isDigit(lexer.peek()) || lexer.peek() == '-' || lexer.peek() == '.') {
            char c = lexer.next();
            if (c == '.') {
                isFloatingPoint = true;
            }
            sb.append(c);
        }
        //Numeric type conversion to Double and Long
        if (isFloatingPoint) {
            return Double.parseDouble(sb.toString());
        } else {
            return Long.parseLong(sb.toString());
        }
    }
}

/**
 * This is a simple JSON lexer (lexical analyzer). It handles the logic for extracting characters from a JSON string.
 */
class JsonLexer {
    private String input;
    private int position;

    public JsonLexer(String input) {
        this.input = input;
        this.position = 0;
    }

    /**
     * This method returns the current character at the position after skipping whitespace.
     *
     * @return The current character.
     */
    public char peek() {
        skipWhitespace();
        if (position < input.length()) {
            return input.charAt(position);
        }
        return 0;
    }

    /**
     * This method returns the current character and advances the position by one character.
     *
     * @return The current character.
     */
    public char next() {
        char c = peek();
        position++;
        return c;
    }

    /**
     * This method skips whitespace characters.
     */
    public void skipWhitespace() {
        while (position < input.length() && Character.isWhitespace(input.charAt(position))) {
            position++;
        }
    }

    /**
     * This method checks if the current position matches the specified string.
     *
     * @param str The string to match.
     * @return True if the current position matches the string, false otherwise.
     */
    public boolean matches(String str) {
        return input.startsWith(str, position);
    }

    /**
     * This method consumes and validates the current character.
     *
     * @param expected The expected character.
     * @throws JsonParseException If the consumed character does not match the expected character.
     */
    public void consume(char expected) throws JsonParseException {
        char actual = next();
        if (actual != expected) {
            throw new JsonParseException("Expected '" + expected + "', but found '" + actual + "'");
        }
    }

    /**
     * This method consumes and validates a sequence of characters.
     *
     * @param expected The expected sequence of characters.
     * @throws JsonParseException If the consumed characters do not match the expected sequence.
     */
    public void consume(String expected) throws JsonParseException {
        for (char c : expected.toCharArray()) {
            consume(c);
        }
    }
}
